use koto_runtime::{Ptr, Result, derive::*, prelude::*};

pub fn make_module() -> KMap {
    let result = KMap::with_type("regex");

    result.add_fn("new", |ctx| match ctx.args() {
        [KValue::Str(pattern)] => Ok(Regex::new(pattern)?.into()),
        unexpected => unexpected_args("|String|", unexpected),
    });

    result
}

#[derive(Clone, Debug, KotoType, KotoCopy)]
#[koto(runtime = koto_runtime)]
pub struct Regex(Ptr<regex::Regex>);

#[koto_impl(runtime = koto_runtime)]
impl Regex {
    pub fn new(pattern: &str) -> Result<Self> {
        match regex::Regex::new(pattern) {
            Ok(r) => Ok(Self(r.into())),
            Err(e) => runtime_error!("failed to parse regex pattern: {e}"),
        }
    }

    #[koto_method]
    fn is_match(&self, text: &str) -> bool {
        self.0.is_match(text)
    }

    #[koto_method]
    fn find(&self, text: &KString) -> KValue {
        let m = self.0.find(text.as_str());
        match m {
            Some(m) => Match::make_value(text.clone(), m.start(), m.end()),
            None => KValue::Null,
        }
    }

    #[koto_method]
    fn find_all(&self, text: &KString) -> KValue {
        let matches: Vec<(usize, usize)> = self
            .0
            .find_iter(text)
            .map(|m| (m.start(), m.end()))
            .collect();

        if matches.is_empty() {
            KValue::Null
        } else {
            Matches {
                text: text.clone(),
                matches,
                last_index: 0,
            }
            .into()
        }
    }

    #[koto_method]
    fn captures(&self, text: &KString) -> KValue {
        let Some(captures) = self.0.captures(text) else {
            return KValue::Null;
        };

        let mut result = ValueMap::with_capacity(captures.len());

        for (i, (capture, name)) in captures.iter().zip(self.0.capture_names()).enumerate() {
            if let Some(capture) = capture {
                let match_ = Match::make_value(text.clone(), capture.start(), capture.end());

                if let Some(name) = name {
                    // Also insert the match with the capture group's name
                    result.insert(name.into(), match_);
                } else {
                    // Insert the match with the capture group's index
                    result.insert(i.into(), match_);
                }
            } else {
                result.insert(i.into(), KValue::Null);
            }
        }

        KMap::from(result).into()
    }

    #[koto_method]
    fn replace_all(&self, text: &str, replacement: &str) -> String {
        self.0.replace_all(text, replacement).to_string()
    }
}

impl KotoObject for Regex {}

impl From<Regex> for KValue {
    fn from(regex: Regex) -> Self {
        KObject::from(regex).into()
    }
}

#[derive(Clone, Debug, KotoType, KotoCopy)]
#[koto(runtime = koto_runtime)]
pub struct Matches {
    text: KString,
    matches: Vec<(usize, usize)>,
    last_index: usize,
}

impl Matches {}

impl KotoAccess for Matches {}

impl KotoObject for Matches {
    fn is_iterable(&self) -> IsIterable {
        IsIterable::ForwardIterator
    }

    fn iterator_next(&mut self, _vm: &mut KotoVm) -> Option<KIteratorOutput> {
        if self.last_index >= self.matches.len() {
            self.last_index = 0;
            None
        } else {
            let result = match self.matches.get(self.last_index) {
                Some((start, end)) => Some(KIteratorOutput::Value(Match::make_value(
                    self.text.clone(),
                    *start,
                    *end,
                ))),
                None => None,
            };

            self.last_index += 1;
            result
        }
    }
}

impl From<Matches> for KValue {
    fn from(matches: Matches) -> Self {
        KObject::from(matches).into()
    }
}

#[derive(Clone, Debug, KotoType, KotoCopy)]
#[koto(runtime = koto_runtime)]
pub struct Match {
    text: KString,
    bounds: KRange,
}

#[koto_impl(runtime = koto_runtime)]
impl Match {
    pub fn make_value(matched: KString, start: usize, end: usize) -> KValue {
        let Some(text) = matched.with_bounds(start..end) else {
            return KValue::Null;
        };

        Self {
            text,
            bounds: KRange::from(start as i64..end as i64),
        }
        .into()
    }

    #[koto_method]
    fn text(&self) -> KString {
        self.text.clone()
    }

    #[koto_method]
    fn range(&self) -> KRange {
        self.bounds.clone()
    }
}

impl KotoObject for Match {
    fn display(&self, ctx: &mut DisplayContext) -> Result<()> {
        ctx.append(format!("Match('{}', {})", self.text, self.bounds));
        Ok(())
    }
}

impl From<Match> for KValue {
    fn from(match_: Match) -> Self {
        KObject::from(match_).into()
    }
}
